#!/usr/bin/env perl


package Money;

use overload
    '""'  => 'stringify',
    '0+'  => 'as_number',
    '+'   => 'add',
    '-'   => 'subtract',
    '*'   => 'multiply',
    '/'   => 'divide',
    '<=>' => 'num_cmp';


use Class::MethodMaker
    new_hash_with_init => 'new';

sub init {}

# getter and setter separately so subclasses can override (needed for
# Money::Inflated)

sub _get_amount { $_[0]->{amount}         }
sub _set_amount { $_[0]->{amount} = $_[1] }

sub amount {
    my $self = shift;
    return $self->_get_amount unless @_;
    $self->_set_amount(shift);
}

sub stringify { $_[0]->amount }
sub as_number { $_[0]->amount }


my %ops = (
    add      => '+',
    subtract => '-',
    multiply => '*',
    divide   => '/',
);

while (my ($sub, $op) = each %ops) {
    no strict 'refs';
    *{$sub} = sub {
        my ($self, $other, $reversed) = @_;
        my $self_amount = $self->amount;
        my $other_amount =
            UNIVERSAL::isa($other, 'Money') ? $other->amount: $other;
        ($self_amount, $other_amount) = ($other_amount, $self_amount)
            if $reversed;

        # make a new object, don't reuse the old one
        my $package = ref $self;
        my $result = $package->new(amount =>
            eval "\$self_amount $op \$other_amount");
        die $@ if $@;
        $result;
    }
}


sub num_cmp {
    my ($self, $other, $reversed) = @_;
    my $self_amount = $self->amount;
    my $other_amount =
        UNIVERSAL::isa($other, 'Money') ? $other->amount: $other;
    ($self_amount, $other_amount) = ($other_amount, $self_amount)
        if $reversed;
    $self_amount <=> $other_amount;
}



package Money::Inflated;

our @ISA = 'Money';

# Inflation is a class method, as it equally affects all inflated money values
# across the plan.

use Class::MethodMaker
    static_get_set => 'compound_inflation';

sub init {
    my $self = shift;
    $self->SUPER::init(@_);
    $self->compound_inflation(1) unless defined $self->compound_inflation;
}


sub inflate_by {
    my ($self, $inflation) = @_;
    $self->compound_inflation($self->compound_inflation * $inflation);
}

sub _get_amount {
    my $self = shift;
    $self->SUPER::_get_amount() * $self->compound_inflation;
}


package Schedule::Cron;

use Time::ParseDate;
use Data::Dumper;

use strict;

our $DEBUG = 0;

my @WDAYS = qw(
		 Sunday
		 Monday
		 Tuesday
		 Wednesday
		 Thursday
		 Friday
		 Saturday
		 Sunday
		);

my @ALPHACONV = (
		 { },
		 { },
		 { },
		 { qw(jan 1 feb 2 mar 3 apr 4 may 5 jun 6 jul 7 aug 8
		      sep 9 oct 10 nov 11 dec 12) },
		 { qw(sun 0 mon 1 tue 2 wed 3 thu 4 fri 5 sat 6)}
		);
my @RANGES = ( 
	      [ 0,59 ],
	      [ 0,23 ],
	      [ 0,31 ],
	      [ 0,12 ],
	      [ 0,7  ]
	     );

my @LOWMAP = ( 
	      {},
	      {},
	      { 0 => 1},
	      { 0 => 1},
	      { 7 => 0}
	     );

sub new { 
  my $class = shift;
  my $dispatcher = shift || die "No dispatching sub provided";
  die "Dispatcher not a ref to a subroutine" unless ref($dispatcher) eq "CODE";
  my $cfg = ref($_[0]) eq "HASH" ? $_[0] : {  @_ };
  my $self = { 
	      cfg => $cfg,
	      dispatcher => $dispatcher,
	      queue => [ ],
	      map => { }
	     };
  bless $self,(ref($class) || $class);

  $self;
}


sub get_time {
    my $self = shift;
    $self->{_fasttime} ||= time
}

sub get_month { (localtime($_[0]->get_time))[4] + 1 }
sub get_year  { (localtime($_[0]->get_time))[5] + 1900 }

sub get_date_string {
    my $self = shift;
    sprintf '%04d/%02d/%02d',
        sub { $_[5]+1900, $_[4]+1, $_[3] }->(localtime($self->get_time));
}


sub add_entry { 
  my $self = shift;
  my $time = shift;
  my $args = shift; 
  my $dispatch;


  if (ref($args) eq "HASH") {
    my $cfg = $args;
    $args = undef;
    $dispatch = $cfg->{subroutine} || $cfg->{sub};
    $args = $cfg->{arguments} || $cfg->{args};
    if ($cfg->{eval} && $cfg) {
      die "You have to provide a simple scalar if using eval" if (ref($args));
      $args = [ eval $args ];
      die "Cannot evaluate args ($args)"
	if $@;
    }
  } elsif (ref($args) eq "CODE") {
    $dispatch = $args;
    $args = shift;
  }


  if (ref($args) ne "ARRAY") {
    $args = [ $args,@_ ];
  }

  $dispatch ||= $self->{dispatcher};

  push @{$self->{time_table}},$time;
  push @{$self->{args}},[$dispatch,$args];


  my $index = $#{$self->{time_table}};
  my $id = $args->[0];
  $self->{map}->{$id} = $index if $id;

  return $#{$self->{time_table}};
}

sub run { 
  my $self = shift;
  my $cfg = ref($_[0]) eq "HASH" ? $_[0] : {  @_ };
  
  $self->build_initial_queue;
  die "Nothing in schedule queue" unless @{$self->{queue}};

    while (!$self->{exit}) {
        my ($index,$time) = @{shift @{$self->{queue}}};
        $0 = "Schedule::Cron MainLoop - next: ".scalar(localtime($time));
        die "No time found" unless $time;
        my $now = $self->get_time;
        while ($time > $now) {
            $self->{_fasttime} += $time-$now;
            $now = $self->get_time;
        }
        $self->execute($index);
        $self->update_queue($index);
    } 

    # note that 'Grand Total' will be sorted after numeric years, so that's ok

    for my $year (sort keys %{ $self->totals }) {
        print "\nSummary for $year:\n\n";
        my ($annual_total, $in_total, $out_total) = (0, 0, 0);

        # sort entries by value
        my @annual =
            sort { $a->[1] <=> $b->[1] }
            map  { [ $_, $self->totals->{$year}{$_} ] }
            keys %{ $self->totals->{$year} };

        for my $item (@annual) {
            printf "%-40s %10.2f\n", @$item;
            my $amount = $item->[1];
            $annual_total += $amount;
            if ($amount < 0) {
                $out_total += $amount;
            } else {
                $in_total += $amount;
            }
        }
        print "\nTotals for $year:\n";
        printf "%-40s %10.2f\n",   'Total In', $in_total;
        printf "%-40s %10.2f\n",   'Total Out', $out_total;
        printf "%-40s %10.2f\n\n", 'Total Balance', $annual_total;
    }
}


sub get_next_execution_time { 
  my $self = shift;
  my $cron_entry = shift;
  my $time = shift;
  
  $cron_entry = [ split /\s+/,$cron_entry ] unless ref($cron_entry);


  # Expand and check entry:
  # =======================
  die "Exactly 5 columns has to be specified for a crontab entry ! (not ",
  scalar(@$cron_entry),")"
    unless $#$cron_entry == 4;
  
  my @expanded;
  my $w;

  for my $i (0..4) {
    my @e = split /,/,$cron_entry->[$i];
    my @res;
    my $t;
    while (defined($t = shift @e)) {
      if ($t =~ m|^([^-]+)-([^-/]+)(/(.*))?$|) {
	my ($low,$high,$step) = ($1,$2,$4);
	$step = 1 unless $step;
	if ($low !~ /^(\d+)/) {
	  $low = $ALPHACONV[$i]{lc $low};
	}
	if ($high !~ /^(\d+)/) {
	  $high = $ALPHACONV[$i]{lc $high};
	}
	if (! defined($low) || !defined($high) ||  $low > $high || $step !~ /^\d+$/) {
	  die "Invalid cronentry '",$cron_entry->[$i],"'";
	}
	my $j;
	for ($j = $low; $j <= $high; $j += $step) {
	  push @e,$j;
	}
      } else {
	$t = $ALPHACONV[$i]{lc $t} if $t !~ /^(\d+|\*)$/;
	$t = $LOWMAP[$i]{$t} if exists($LOWMAP[$i]{$t});
	
	die "Invalid cronentry '",$cron_entry->[$i],"'" 
	  if (!defined($t) || ($t ne '*' && ($t < $RANGES[$i][0] || $t > $RANGES[$i][1])));
	push @res,$t;
      }
    }
    push @expanded, [ sort { $a <=> $b} @res];
  }

  # Calculating time:
  # =================
  my $now = $time || $self->get_time;

  if ($expanded[2]->[0] ne '*' && $expanded[4]->[0] ne '*') {
    # Special check for which time is lower (Month-day or Week-day spec):
    my @bak = @{$expanded[4]};
    $expanded[4] = [ '*' ];
    my $t1 = $self->calc_time($now,\@expanded);
    $expanded[4] = \@bak;
    $expanded[2] = [ '*' ];
    my $t2 = $self->calc_time($now,\@expanded);
    return $t1 < $t2 ? $t1 : $t2;
  } else {
    # No conflicts possible:
    return $self->calc_time($now,\@expanded);
  }
}

# ==================================================
# PRIVATE METHODS:
# ==================================================

# Build up executing queue and delete any
# existing entries
sub build_initial_queue { 
  my $self = shift;
  $self->{queue} = [ ];
  for my $id (0..$#{$self->{time_table}}) {
    $self->update_queue($id);
  }
}


# Execute a subroutine whose time has come
sub execute {
    my ($self, $index) = @_;

# We don't want to fork as we only have simple subs and we want to share
# variables between the subs. So this is only a simple patch; assume we
# have a dispatcher sub and don't care about args (cf. the superclass's
# execute() method)

    $self->{args}[$index][0]->();
}


# Udate the scheduler queue with a new entry
sub update_queue { 
  my $self = shift;

  my $index = shift;
  
  my $entry = $self->{time_table}->[$index];
  
  $entry = [ split(/\s+/,$entry) ] unless ref($entry);
  my $new_time = $self->get_next_execution_time($entry);
  $self->{queue} = [ sort { $a->[1] <=> $b->[1] } @{$self->{queue}},[$index,$new_time] ];
}


# The heart of the module.
# calulate the next concrete date
# for execution from a cronta entry
sub calc_time { 
  my $self = shift;
  my $now = shift;
  my $expanded = shift;
  
  my ($now_min,$now_hour,$now_mday,$now_mon,$now_wday,$now_year) = 
    (localtime($now+60))[1,2,3,4,6,5];
  $now_mon++; 
  $now_year += 1900;

  # Notes on variables set:
  # $now_... : the current date, fixed at call time
  # $dest_...: date used for backtracking. At the end, it contains
  #            the desired lowest matching date

  my ($dest_mon,$dest_mday,$dest_wday,$dest_hour,$dest_min,$dest_year) = 
    ($now_mon,$now_mday,$now_wday,$now_hour,$now_min,$now_year);

  while ($dest_year <= $now_year + 1) { # Airbag...
    

    # Check month:
    if ($expanded->[3]->[0] ne '*') {
      unless (defined ($dest_mon = $self->get_nearest($dest_mon,$expanded->[3]))) {
	$dest_mon = $expanded->[3]->[0];
	$dest_year++;
      } 
    } 
  
    # Check for day of month:
    if ($expanded->[2]->[0] ne '*') {		
      if ($dest_mon != $now_mon) {	
 	$dest_mday = $expanded->[2]->[0];
      } else {
	unless (defined ($dest_mday = $self->get_nearest($dest_mday,$expanded->[2]))) {
	  # Next day matched is within the next month. ==> redo it
	  $dest_mday = $expanded->[2]->[0];
	  $dest_mon++;
	  if ($dest_mon > 12) {
	    $dest_mon = 1;
	    $dest_year++;
	  }
	  next;
	}
      }
    } else {
      $dest_mday = ($dest_mon == $now_mon ? $dest_mday : 1);
    }
  
    # Check for day of week:
    if ($expanded->[4]->[0] ne '*') {
      $dest_wday = $self->get_nearest($dest_wday,$expanded->[4]);
      $dest_wday = $expanded->[4]->[0] unless $dest_wday;
    
      my ($mon,$mday,$year);
      $dest_mday = 1 if $dest_mon != $now_mon;
      my $t = parsedate(sprintf("%4.4d/%2.2d/%2.2d",$dest_year,$dest_mon,$dest_mday));
      ($mon,$mday,$year) =  
	(localtime(parsedate("$WDAYS[$dest_wday]",PREFER_FUTURE=>1,NOW=>$t-1)))[4,3,5]; 
      $mon++;
      $year += 1900;

      if ($mon != $dest_mon || $year != $dest_year) {
	$dest_mon = $mon;
	$dest_year = $year;
	$dest_mday = 1;
	$dest_wday = (localtime(parsedate(sprintf("%4.4d/%2.2d/%2.2d",
						  $dest_year,$dest_mon,$dest_mday))))[6];
	next;
      }
      
      $dest_mday = $mday;
    } else {
      unless ($dest_mday) {
	$dest_mday = ($dest_mon == $now_mon ? $dest_mday : 1);
      }
    }
  
    # Check for hour
    if ($expanded->[1]->[0] ne '*') {
      if ($dest_mday != $now_mday) {
	$dest_hour = $expanded->[1]->[0];
      } else {
	unless (defined ($dest_hour = $self->get_nearest($dest_hour,$expanded->[1]))) {
	  # Hour to match is at the next day ==> redo it
	  $dest_hour = $expanded->[1]->[0];
	  my $t = parsedate(sprintf("%2.2d:%2.2d %4.4d/%2.2d/%2.2d",
				    $dest_hour,$dest_min,$dest_year,$dest_mon,$dest_mday));
	  ($dest_mday,$dest_mon,$dest_year,$dest_wday) = 
	    (localtime(parsedate("+ 1 day",NOW=>$t)))[3,4,5,6];
	  $dest_mon++; 
	  $dest_year += 1900;
	  next; 
	}
      }
    } else {
      $dest_hour = ($dest_mday == $now_mday ? $dest_hour : 0);
    }
    
    # Check for minute
  if ($expanded->[0]->[0] ne '*') {
      if ($dest_hour != $now_hour) {
	$dest_min = $expanded->[0]->[0];
      } else {
	unless (defined ($dest_min = $self->get_nearest($dest_min,$expanded->[0]))) {
	  # Minute to match is at the next hour ==> redo it
	  $dest_min = $expanded->[0]->[0];
	  my $t = parsedate(sprintf("%2.2d:%2.2d %4.4d/%2.2d/%2.2d",
				    $dest_hour,$dest_min,$dest_year,$dest_mon,$dest_mday));
	  ($dest_hour,$dest_mday,$dest_mon,$dest_year,$dest_wday) = 
	    (localtime(parsedate(" + 1 hour",NOW=>$t)))  [2,3,4,5,6];
	  $dest_mon++;
	  $dest_year += 1900;
	  next;
	}
      }
    } else {
      $dest_min = ($dest_hour == $now_hour ? $dest_min : 0);
    }
    
    # We did it !!
    return parsedate(sprintf("%2.2d:%2.2d %4.4d/%2.2d/%2.2d",
			     $dest_hour,$dest_min,$dest_year,$dest_mon,$dest_mday));
  }
}

# get next entry in list or 
# undef if is the highest entry found
sub get_nearest { 
  my $self = shift;
  my $x = shift;
  my $to_check = shift;
  foreach my $i (0 .. $#$to_check) {
    if ($$to_check[$i] >= $x) {
      return $$to_check[$i] ;
    }
  }
  return undef;
}


package FinancialPlan;

use Date::Calc qw/Mktime Localtime/;

our @ISA = 'Schedule::Cron';

use Class::MethodMaker
    get_set => 'money',
    hash    => 'totals';

sub new {
    my $self = Schedule::Cron->new(sub { print "dispatch <@_>\n" });
    bless $self, 'FinancialPlan';

    $self->add_entry("0 0 1 * *", sub {
        print "\n";
        $self->print_item(Subtotal => $self->money, 1);
        print "\n";
    });

    # can't go beyond 18 Feb 2037, because of the year 2038 bug?
    # but perl -le'print scalar localtime 2**31-1'
    #   Tue Jan 19 04:14:07 2038
    # hmmm...

    $self->add_entry("0 0 1 1 *", sub { $self->{exit}++ if $self->get_year == 2037 });

    $self;
}

sub make_date {
    my ($self, $year, $month, $date) = @_;
    Mktime($year, $month, $date, 0, 0, 0);
}

sub add_amount {
    my ($self, $amount) = @_;
    $self->money($self->money + $amount);
}

sub add_weekly {
    my ($self, $week_day, $item, $amount, $from, $until) = @_;
    my $code_amount = ref $amount eq 'CODE' ? $amount : sub { $amount };
    $self->add_entry("0 0 * * $week_day", sub {
        return if defined($from)  && $self->get_time < $from;
        return if defined($until) && $self->get_time > $until;
        my $amount = $code_amount->();
        $self->add_amount($amount);
        $self->print_item($item, $amount);
    });
}

sub add_monthly {
    my ($self, $mday, $item, $amount, $from, $until) = @_;
    $self->add_annual($mday, '*', $item, $amount, $from, $until);
}

sub add_annual {
    my ($self, $mday, $month, $item, $amount, $from, $until) = @_;
    my $code_amount = ref $amount eq 'CODE' ? $amount : sub { $amount };
    $self->add_entry("0 0 $mday $month *", sub {
        return if defined($from)  && $self->get_time < $from;
        return if defined($until) && $self->get_time > $until;
        my $amount = $code_amount->();
        $self->add_amount($amount);
        $self->print_item($item, $amount);
    });
}

sub add_monthly_repayment {
    my ($self, $mday, $item, $rest_amount, $rate, $from, $until) = @_;
    my $code_rate = ref $rate eq 'CODE' ? $rate : sub { $rate };
    $self->add_entry("0 0 $mday * *", sub {
        return if $rest_amount <= 0;

        return if defined($from)  && $self->get_time < $from;

        # at the end, repay the rest.
        if (defined($until) && $self->get_time > $until) {
            $self->add_amount(-$rest_amount);
            $rest_amount = 0;
            $self->print_item($item, $rest_amount);
            return;
        }

        my $rate = $code_rate->();
        $self->add_amount(-$rate);
        $rest_amount -= $rate;
        $self->print_item_ext($item, "(Rest -$rest_amount)", -$rate);
    });
}


sub add_once {
    my $self = shift;
    my ($mday, $month, $year, $item, $amount);
    # also accept a time number (i.e, generated by Mktime())
    if ($_[0] =~ /^\d{10}$/) {
        my $date = shift;
        ($year, $month, $mday) = Localtime($date);
        ($item, $amount) = @_;
    } else {
        ($mday, $month, $year, $item, $amount) = @_;
    }

    my $code_amount = ref $amount eq 'CODE' ? $amount : sub { $amount };
    $self->add_entry("0 0 $mday $month *" => sub {
        return unless $self->get_year == $year;
        my $amount = $code_amount->();
        $self->add_amount($amount);
        $self->print_item($item => $amount);
    });
}


sub print_item {
    my ($self, $item, $amount, $skip_total) = @_;
    $self->print_item_ext($item, '', $amount, $skip_total);
}


sub print_item_ext {
    my ($self, $item, $additional_info, $amount, $skip_total) = @_;
    printf "%s: %-40s %10.2f\n",
        $self->get_date_string, "$item $additional_info", $amount;
    return if $skip_total;

    $self->totals->{ $self->get_year }{$item} += $amount;
    $self->totals->{'Grand Total'}{$item} += $amount;
}


package main;

# Nicht eingerechnet:
# - Kosten fuer Teilnahme an Go-Turnieren und EGCs? Velden EUR 200,-
# - Abfertigung: Bei Einmalauszahlung 6% Steuer, sonst steuerfreie
#   Zusatzpension
# - Teilbetraege besser verzinst anlegen
# - Arbeitslosengeld bzw. Notstandshilfe nach Arbeitsende
# - kleinere Nebenjobs nach Arbeitsende
# - Medizinische Kosten
# - Wohnung Herklotzgasse beibehalten?
# - Erbe
#
# - guenstig einkaufen bei fabriksabverkauf (factory outlet)
#   - Nestle: 5., Emil-Kralik-Gasse 6, Ecke Guertel, Eingang Guertel
#   - Manner, 17., Wilhelminenstr. 6
#   - Pischinger: 10., Himberger Str 2
# - http://www.schnaeppchenfuehrer.at/
#
# TODO:
# - Union-Pensionsversicherung: stimmen meine Annahmen?
# - Kontrolliere Stromrechnung: Seit Dez. 2005 monatlich 38,40 abgebucht
# - Bessere Zinsen bei Deniz Bank oder IngDiBa, aber Sicherheit nur bis
#   EUR 20000,- (wie bei allen Banken)

use warnings;
use strict;
use Date::Calc 'Localtime';

$|++;


sub min {
    my ($a, $b) = @_;
    $a < $b ? $a : $b;
}


# my $inflation = 1.02;
my $inflation = 1.00;    # Gib Betraege nach heutigem Wert aus

# Essen und Unterhaltung:
#
# Monatliches Kostgeld fuer Mama: 130,-
# Mittagessen
# Getraenke in Lokalen, Kaffeehaeusern
# Restaurants (Chinesisch usw.)
# audiogolessons.com
# Buecher
# evtl. Uebernachtungen und Bahnkosten bei Turnieren und Gocamps

my $essen_unterhaltung    = Money::Inflated->new(amount => -100);

my $haushaltsversicherung = Money::Inflated->new(amount => -14);
my $jahreskarte           = Money::Inflated->new(amount => -458);
my $weihnachtsausgaben    = Money::Inflated->new(amount => -400);
my $fahrradversicherung   = Money::Inflated->new(amount => -80);

my $daungasse_kosten      = Money::Inflated->new(amount => -150);
my $daungasse_gas         = Money::Inflated->new(amount => -118); # 5x p.a.
my $daungasse_strom       = Money::Inflated->new(amount => -40);  # monthly

my $herklotzgasse_kosten  = Money::Inflated->new(amount => -400);
my $herklotzgasse_waerme  = Money::Inflated->new(amount => -20);  # monthly
my $herklotzgasse_strom   = Money::Inflated->new(amount => -20);  # monthly
my $herklotzgasse_wasser  = Money::Inflated->new(amount => -10); # monthly
my $herklotzgasse_gis     = Money::Inflated->new(amount => -20);  # monthly
# chello inkl. telekabel
my $herklotzgasse_chello  = Money::Inflated->new(amount => -40);  # monthly
my $chello_light          = Money::Inflated->new(amount => -22);  # monthly

my $friseur               = Money::Inflated->new(amount => -12);
my $selbstversicherung    = Money::Inflated->new(amount => -50);
my $verschiedenes         = Money::Inflated->new(amount => -100); # monthly

my $go_world_abo          = Money::Inflated->new(amount => -26);
my $goklub_gote           = Money::Inflated->new(amount => -20);

my $fruehlingsurlaub      = Money::Inflated->new(amount => -1000);
my $wohnungsverkauf       = Money::Inflated->new(amount => 180_000);

# Projezierte Pension nach heutigem Wert, daher inflationsbereinigt.
# Wert vielleicht zu konservativ?
my $pension               = Money::Inflated->new(amount => 1000);

# 30 Stunden pro Woche
my $gehalt_brutto         = Money::Inflated->new(amount => 2443);
my $gehalt                = Money::Inflated->new(amount => 1584);

my $plan = FinancialPlan->new;

my $datum_arbeitsende = $plan->make_date(2022, 02, 28);
my $datum_pension         = $plan->make_date(2033, 3, 1);
my $datum_wohnungsverkauf = $plan->make_date(2028, 3, 1);


# support for inflation-based amounts
$plan->add_entry("0 0 31 12 *", sub {
    Money::Inflated->inflate_by($inflation);
});

$plan->money(Money->new(amount => 1000.00));
$plan->print_item(Startkapital => $plan->money);

$plan->add_weekly('Mon', 'Essen/Unterhaltung'  => sub { $essen_unterhaltung });
$plan->add_monthly( 1, Verschiedenes         => sub { $verschiedenes });

# will be about the same in Daungasse and Herklotzgasse
$plan->add_monthly(16, Haushaltsversicherung => sub { $haushaltsversicherung });

# Die Haelfte des Geldes ist auf der Easybank: 1.75% p.a. minus 25% KESt,
# monatlich berechnet. Die andere Haelfte ist angelegt, zu 7% p.a.
# (konservativ rechnen ist besser hier).

$plan->add_monthly( 1, 'Zinsen Konto' => sub {
    $plan->money * (1/2) * 0.0175 * 0.75 / 12
});
$plan->add_monthly( 1, 'Zinsen Investitionen' => sub {
    $plan->money * (1/2) * 0.07 * 0.75 / 12
});

# Kontofuehrungsgebuehr 4,- pro Monat, wenn Durchschnittsbetrag < 2500,-
$plan->add_entry("0 0 28 * *", sub {
    return if $plan->money >= 2500;
    my $kontofuehrung = -4;
    $plan->add_amount($kontofuehrung);
    $plan->print_item(Kontofuehrung => $kontofuehrung);
});

$plan->add_annual( 5,  5, Pensionsversicherung =>
    sub { -1471.30 * 1.04 ** (min($plan->get_year, 2028) - 2006) },
    undef, $datum_pension);

$plan->add_annual( 1,  1, 'Go World Abonnement' => sub { $go_world_abo });
$plan->add_annual( 1,  '1,7', 'Goklub Gote' => sub { $goklub_gote });

$plan->add_annual( 1, 12, Jahreskarte         => sub { $jahreskarte });
$plan->add_annual(10, 12, Weihnachtsausgaben  => sub { $weihnachtsausgaben });
$plan->add_annual(15, 11, Fahrradversicherung => sub { $fahrradversicherung });

# Nicht inflationsbereinigt, weil Kleidungspreise nicht so stark wachsen.
$plan->add_annual(15, 11, Kleidungsruecklage => - 300);

# Alle drei Jahre ein neuer Laptop zu 1200,-. Nicht inflationsbereinigt, weil
# Hardware in der untersten Kategorie immer in etwa dasselbe kostet.
$plan->add_annual(15, 11, Hardwareruecklage => - 400);

$plan->add_monthly(5, 'Daungasse Betriebskosten' =>
    sub { $daungasse_kosten }, undef, $datum_wohnungsverkauf);
$plan->add_annual(28, '1,3,6,9,11', 'Daungasse Gas' =>
    sub { $daungasse_gas }, undef, $datum_wohnungsverkauf);
$plan->add_monthly(28, 'Daungasse Strom' =>
    sub { $daungasse_strom }, undef, $datum_wohnungsverkauf);

$plan->add_monthly(1, 'Herklotzgasse Miete' =>
    sub { $herklotzgasse_kosten }, $datum_wohnungsverkauf);
$plan->add_monthly(1, 'Herklotzgasse Fernwaerme' =>
    sub { $herklotzgasse_waerme }, $datum_wohnungsverkauf);
$plan->add_monthly(1, 'Herklotzgasse Strom' =>
    sub { $herklotzgasse_strom }, $datum_wohnungsverkauf);
$plan->add_monthly(1, 'Herklotzgasse Wasser' =>
    sub { $herklotzgasse_wasser }, $datum_wohnungsverkauf);
$plan->add_monthly(1, 'Herklotzgasse GIS' =>
    sub { $herklotzgasse_gis }, $datum_wohnungsverkauf);
$plan->add_monthly(1, 'Herklotzgasse Chello' =>
    sub { $herklotzgasse_chello }, $datum_wohnungsverkauf);

# Chello light in der Herklotzgasse: Bis ich die Wohnung verkaufe, dann
# Chello+Telekabel, siehe oben.
#
# Chello light in der Daungasse: Ab Arbeitsende, bis zum Wohnungsverkauf

$plan->add_monthly(1, 'Herklotzgasse Chello light' =>
    sub { $chello_light }, undef, $datum_wohnungsverkauf);
$plan->add_monthly(1, 'Daungasse Chello light' =>
    sub { $chello_light }, $datum_arbeitsende, $datum_wohnungsverkauf);

$plan->add_annual(1, '2,4,6,8,10,12', Friseur => sub { $friseur });

# Buchen einer Urlaubsreise fuer Ende Mai
$plan->add_annual(1, 5, Fruehlingsurlaub => sub { $fruehlingsurlaub });

# Wohnungsverbesserungen ca. 7000, ab Kreditende

$plan->add_monthly_repayment(
    1, Wohnungsverbesserungen => 7_000, 200, $plan->make_date(2007,6,1));
$plan->add_monthly_repayment(
    1, 'Kredit Mama', 32_000, 500, $plan->make_date(2007,8,1));

# my %work_less = (mday => 1, month => 5, year => 2004);
# $plan->add_entry("0 0 $work_less{mday} $work_less{month} *" => sub {
#     return unless $plan->get_year == $work_less{year};
#     $gehalt->amount(1429);  # 2137 brutto
# });

$plan->add_monthly( 15, Gehalt => sub { $gehalt }, undef, $datum_arbeitsende);
my $extragehalt_prozent = 1.241;
$plan->add_annual( 7,  5, Urlaubsgeld =>
    sub { $gehalt * $extragehalt_prozent }, undef, $datum_arbeitsende);
$plan->add_annual( 7, 11, Weihnachtsgeld =>
    sub { $gehalt * $extragehalt_prozent }, undef, $datum_arbeitsende);
$plan->add_annual(15,  1, 'Weihnachtsgeschenk von Firma' => 200,
    undef, $datum_arbeitsende);

# Arbeitnehmerveranlagung. Fuer 2005 habe ich bei ca. 1425 EUR netto pro Monat
# etwa 161 EUR zurueckerhalten, mit Pauschale und Pensionsversicherung. Also
# nehme - da ich es nicht genau berechnen kann - ich 10% eines
# Nettomonatsgehalts an.

$plan->add_annual( 1,  4, Arbeitnehmerveranlagung =>
    sub { $gehalt / 10 }, undef, $datum_arbeitsende);

# Nach dem Arbeitsende muss ich das Mobiltelephon wieder selbst bezahlen.
# Nicht inflationsbereinigt, da Telekommunikationskosten eher gleichbleiben
# als steigen.

$plan->add_monthly(19, Mobiltelephon => -20, $datum_arbeitsende);

# Selbstversicherung via geringfuegiger Beschaeftigung nach Arbeitsende
$plan->add_monthly(5, Selbstversicherung => sub { $selbstversicherung },
    $datum_arbeitsende, $datum_pension);

$plan->add_monthly(1, Pension => sub { $pension }, $datum_pension);

# Abfertigung (1.53% des Bruttolohns, 14x im Jahr, von Mai 2003 bis
# Arbeitsende):

my $abfertigung_prozent = 0.0153;

# Mai-Juli 2003 (+ 1/2 Urlaub)
my $abfertigung = Money->new(amount => 2850 * 3.5 * $abfertigung_prozent);

$plan->add_entry("0 0 15 * *", sub {
    return unless $plan->get_time < $datum_arbeitsende;
    $abfertigung += $gehalt_brutto * $abfertigung_prozent;

    if ($plan->get_month == 5 || $plan->get_month == 11) {
        $abfertigung +=
            $gehalt_brutto * $extragehalt_prozent * $abfertigung_prozent;
    }
});

$plan->add_once($datum_pension, Abfertigung => sub { $abfertigung });
$plan->add_once($datum_wohnungsverkauf, Wohnungsverkauf =>
    sub { $wohnungsverkauf });

$plan->run;
